#include "znx_log.h"
#include "znx_time.h"

znx_log_t def_log = {
    .log_item_size          = ZNX_MIN_LOG_ITEM_SIZE,
    .level                  = LOG_LEVEL_DEBUG,
    .log_path               = NULL,
    .fd                     = STDOUT_FILENO,
    .rolling                = LOG_ROLLING_DAY,
    .lock                   = ZNX_SPINLOCK_INITLIZER,
    .pending_bufs.prev      = &def_log.pending_bufs,
    .pending_bufs.next      = &def_log.pending_bufs,
    .pending_nums           = 0,
    .active_buf             = NULL,
    .last_switch_ms         = 0,
    .last_check_rolling_ms  = 0,
    .tid                    = 0,
    .mutex                  = ZNX_MUTEX_INITIALIZER,
    .cond                   = ZNX_COND_INITIALIZER,
    .stopped                = 0,
    .busy                   = 0,
    .inited                 = ZNX_FALSE
};


static void
znx_log_real_path_gen(char *buf, size_t buf_size,
    const char *base_path, znx_log_rolling_t rolling_type)
{
    size_t off = 0;
    size_t base_path_len = strlen(base_path);

    znx_memcpy(buf, base_path, base_path_len);
    off += base_path_len;
    buf[off] = '.';
    off += 1;

    time_t time = (time_t)znx_usec_2_sec(get_cache_time());
    struct tm ltm;
    localtime_r(&time, &ltm);

    if (rolling_type == LOG_ROLLING_MINUTE) {
        snprintf(buf + off, buf_size - off, "%04d%02d%02d%02d%02d",
            ltm.tm_year + 1900, ltm.tm_mon + 1, ltm.tm_mday, ltm.tm_hour, ltm.tm_min);
    } else if (rolling_type == LOG_ROLLING_HOUR) {
        snprintf(buf + off, buf_size - off, "%04d%02d%02d%02d",
            ltm.tm_year + 1900, ltm.tm_mon + 1, ltm.tm_mday, ltm.tm_hour);
    } else {
        snprintf(buf + off, buf_size - off, "%04d%02d%02d",
            ltm.tm_year + 1900, ltm.tm_mon + 1, ltm.tm_mday);
    }
}


static inline void
znx_log_switch_buf_unsafe(znx_log_t *log)
{
    znx_queue_insert_head(&log->pending_bufs, &log->active_buf->queue);
    log->pending_nums++;
    log->active_buf = NULL;
}


static inline void
znx_log_try_switch_buf_helper(znx_log_t *log)
{
    if (log->active_buf == NULL) {
        return;
    }

    if (log->active_buf->used == 0) {
        return;
    }

    // fd == STDOUT
    if (log->log_path == NULL) {
        return;
    }

    znx_log_switch_buf_unsafe(log);
}



static inline void
znx_log_try_switch_buf(znx_log_t *log, znx_bool_t stopped)
{
    int64_t now = znx_usec_2_msec(get_cache_time());
    if (!stopped && now - log->last_switch_ms < ZNX_LOG_FLUSH_INTERVAL_MS) {
        return;
    } 

    znx_spinlock_lock(&log->lock);
    znx_log_try_switch_buf_helper(log);
    znx_spinlock_unlock(&log->lock);
}


static inline void
znx_log_try_rolling_helper(znx_log_t *log)
{
    char real_log_path[1280] = { 0 };
    znx_log_real_path_gen(real_log_path, 1280, log->log_path, log->rolling);

    if (strcmp(log->real_log_path, real_log_path) == 0) {
        // no need rolling
        return;
    }

    znx_syscall_result_t res;
    res = znx_syscall_open(real_log_path, O_CREAT|O_APPEND|O_WRONLY, 0644);
    // rolling failed
    if (res.eno != 0) {
        return;
    }

    znx_syscall_unlink(log->log_path);
    znx_syscall_symlink(real_log_path, log->log_path);

    if (log->fd > 0 && log->fd != STDOUT_FILENO) {
        znx_syscall_close(log->fd);
    }

    znx_memcpy(log->real_log_path, real_log_path, strlen(real_log_path));

    log->fd = (int)res.res;
}


static inline void
znx_log_try_rolling(znx_log_t *log, znx_bool_t stopped)
{
    if (stopped) {
        return;
    }

    // fd == STDOUT
    if (log->log_path == NULL) {
        return;
    }

    int64_t now = znx_usec_2_msec(get_cache_time());
    if (now - log->last_check_rolling_ms < ZNX_LOG_CHECK_ROLLING_INTERVAL_MS) {
        return;
    }

    znx_log_try_rolling_helper(log);
    log->last_check_rolling_ms = now;
}


static inline void
znx_log_flush_buf(znx_log_t *log, znx_log_buf_t *log_buf)
{
    write(log->fd, log_buf->buf, log_buf->used);
    znx_log_buf_free(log_buf);
}


static inline void
znx_log_try_flush(znx_log_t *log)
{
    znx_queue_t     *q;
    znx_log_buf_t   *log_buf;

    while (1) {
        q = NULL;
        znx_spinlock_lock(&log->lock);
        if (!znx_queue_empty(&log->pending_bufs)) {
            q = znx_queue_last(&log->pending_bufs);
            znx_queue_remove(q);
            log->pending_nums--;
        }
        znx_spinlock_unlock(&log->lock);

        if (q == NULL) {
            break;
        }

        log_buf = znx_queue_data(q, znx_log_buf_t, queue);
        // wait all async writer done.
        znx_spin_wait_group_wait(&log_buf->wait_group);
        znx_log_flush_buf(log, log_buf);
    }
}


static void
znx_log_do_flush(znx_log_t *log, znx_bool_t stopped)
{
    get_latest_time();
    znx_log_try_switch_buf(log, stopped);
    znx_log_try_rolling(log, stopped);
    znx_log_try_flush(log);

    znx_timespec_t ts = {
        .tv_sec = 10,
        .tv_nsec = 0
    };

    znx_mutex_lock(&log->mutex);
    znx_cond_time_wait(&log->cond, &log->mutex, &ts);
    znx_mutex_unlock(&log->mutex);
}


static void *
znx_log_flush_loop(void *arg)
{
    znx_log_t   *log = arg;
    znx_bool_t  stopped = ZNX_FALSE;

    while (1) {
        stopped = znx_atomic_bool_is_true(&log->stopped);

        znx_log_do_flush(log, stopped);

        if (stopped) {
            break;
        }
    }

    return NULL;
}


static znx_bool_t
znx_log_file_open(znx_log_t *log)
{
    if (log->log_path == NULL) {
        log->fd = STDOUT_FILENO;
        return ZNX_TRUE;
    }

    char real_log_path[1280] = { 0 };
    znx_log_real_path_gen(real_log_path, 1280, log->log_path, log->rolling);

    znx_syscall_result_t res;
    res = znx_syscall_open(real_log_path, O_CREAT|O_APPEND|O_WRONLY, 0644);
    if (res.eno != 0) {
        DD("open %s log file failed, errno: %d", real_log_path, (int)res.eno);
        return ZNX_FALSE;
    }

    znx_syscall_unlink(log->log_path);
    znx_syscall_symlink(real_log_path, log->log_path);
    znx_memcpy(log->real_log_path, real_log_path, strlen(real_log_path));

    if (log->fd > 0 && log->fd != STDOUT_FILENO) {
        znx_syscall_close(log->fd);
    }

    log->fd = (int)res.res;
    return ZNX_TRUE;
}


znx_bool_t
znx_log_init(znx_log_t *log, size_t log_item_size,
    znx_log_level_t level, const char *log_path, znx_log_rolling_t rolling)
{
    if (log->inited) {
        DD("log inited");
        return ZNX_FALSE;
    }

    if (log_item_size < ZNX_MIN_LOG_ITEM_SIZE) {
        log_item_size = ZNX_MIN_LOG_ITEM_SIZE;
    }

    log->log_item_size = log_item_size;
    log->level = level;
    log->rolling = rolling;
    log->log_path = log_path;

    if (log_path != NULL && strlen(log_path) > ZNX_LOG_FILE_PATH_MAX_LEN) {
        DD("the log path length too long");
        return ZNX_FALSE;
    }

    znx_spinlock_init(&log->lock);
    znx_queue_init(&log->pending_bufs);
    log->pending_nums = 0;
    log->active_buf = NULL;
    log->last_switch_ms = znx_usec_2_msec(get_cache_time()) + 1000;
    log->last_check_rolling_ms = znx_usec_2_msec(get_cache_time()) + 1000;
    znx_mutex_init(&log->mutex);
    znx_cond_init(&log->cond);
    log->stopped = 0;
    log->busy = 0;

    if (!znx_log_file_open(log)) {
        return ZNX_FALSE;
    }

    log->tid = znx_thread_create(znx_log_flush_loop, log, ZNX_FALSE);
    znx_thread_set_name(log->tid, "log");
    log->inited = ZNX_TRUE;

    return ZNX_TRUE;
}


static inline void
znx_log_notify(znx_log_t *log)
{
    znx_mutex_lock(&log->mutex);
    znx_cond_signal(&log->cond);
    znx_mutex_unlock(&log->mutex);
}


void
znx_log_flush(znx_log_t *log)
{
    if (log->log_path == NULL) {
        return;
    }

    znx_spinlock_lock(&log->lock);
    znx_log_try_switch_buf_helper(log);
    znx_spinlock_unlock(&log->lock);

    znx_log_notify(log);
}


void
znx_log_stop(znx_log_t *log)
{
    if (log->log_path == NULL) {
        return;
    }

    znx_atomic_bool_set_true(&log->stopped);
    znx_log_notify(log);

    znx_thread_join(log->tid, NULL);
}


void
znx_log_output_buf_hold(znx_log_t *log, size_t len,
    znx_bool_t *need_notify, znx_log_buf_t **hold_buf, size_t *write_pos)
{
    *hold_buf = NULL;
    *write_pos = 0;

    if (log->active_buf
        && log->active_buf->used + len > ZNX_LOG_BUF_SIZE) {
        znx_log_switch_buf_unsafe(log);
        *need_notify = ZNX_TRUE;
    }

    if (log->active_buf == NULL) {
        if (log->pending_nums > ZNX_LOG_MAX_PENDING_NUMS) {
            return;
        }
        log->active_buf = znx_log_buf_create();
        log->last_switch_ms = znx_usec_2_msec(get_cache_time());
    }

    znx_spin_wait_group_add(&log->active_buf->wait_group, 1);
    *hold_buf = log->active_buf;
    *write_pos = log->active_buf->used;
    log->active_buf->used += len;
}


void
znx_log_output(znx_log_t *log, u_char *buf, size_t size)
{
    // fd == STDOUT
    if (log->log_path == NULL) {
        write(log->fd, buf, size);
        return;
    }

    znx_bool_t need_notify = ZNX_FALSE;
    znx_log_buf_t *hold_buf = NULL;
    size_t write_pos = 0;

    while (1) {
        znx_spinlock_lock(&log->lock);
        znx_log_output_buf_hold(log, size,
            &need_notify, &hold_buf, &write_pos);
        znx_spinlock_unlock(&log->lock);

        if (hold_buf != NULL) {
            break;
        }

        znx_atomic_int64_fetch_add(&log->busy, 1);
        usleep(10);
    }

    if (need_notify) {
        znx_log_notify(log);
    }

    znx_memcpy(hold_buf->buf + write_pos, buf, size);
    znx_spin_wait_group_done(&hold_buf->wait_group);
}


void
znx_log_fmt_output(znx_log_t *log, const char *level_str,
    const char *file, int line, znx_bool_t abort_proc,
    const char *fmt, ...)
{
    znx_g_log_buf_grow(log->log_item_size);

    size_t offset = 0;
    g_log_buf[offset++] = '[';
    offset += (size_t)znx_cache_log_time(g_log_buf + offset,
        log->log_item_size - offset);
    g_log_buf[offset++] = ']';
    g_log_buf[offset++] = ' ';

    while (*level_str != '\0') {
        g_log_buf[offset++] = (u_char)(*level_str++);
    }
    g_log_buf[offset++] = '\t';

    file = znx_path_base(file);
    while (*file != '\0') {
        g_log_buf[offset++] = (u_char)(*file++);
    }
    g_log_buf[offset++] = ':';
    offset += (size_t)snprintf((char *)g_log_buf + offset,
        log->log_item_size - offset, "%d\t", line);

    va_list args;
    va_start(args, fmt);
    u_char *start = g_log_buf + offset;
    u_char *last = znx_vsnprintf(start,
        log->log_item_size - offset,
        fmt, args);
    offset += (size_t)(last - start);
    va_end(args);

    g_log_buf[offset++] = '\n';

    znx_log_output(log, g_log_buf, offset);

    if (abort_proc) {
        znx_log_flush(log);
        abort();
    }
}